﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

namespace Dycox.Linq
{
    [TypeConverter(typeof(DateRangeConverter))]
    public struct DateRange : IRange
    {
        public DateRange(DateTime date)
        {
            _upperBound = _lowerBound = date.Date;
            _kind = null;
        }

        public DateRange(DateRangeKind kind)
        {
            if (kind != DateRangeKind.Custom)
                GetRangeBounds(kind, out _lowerBound, out _upperBound);
            else
                _lowerBound = _upperBound = DateTime.MinValue;
            _kind = kind;
        }

        public DateRange(DateTime lower, DateTime upper)
        {
            _upperBound = upper;
            _lowerBound = lower;
            _kind = null;
        }

        static DateRange()
        {
            List<DateRangeKind> list = new List<DateRangeKind>();
            foreach (DateRangeKind kind in Enum.GetValues(typeof(DateRangeKind)))
            {
                if (kind != DateRangeKind.Custom)
                    list.Add(kind);
            }

            PredefinedKinds = list.AsReadOnly();
        }

        public static readonly ICollection<DateRangeKind> PredefinedKinds;

        private DateRangeKind? _kind;

        public DateRangeKind Kind
        {
            get
            {
                if (_kind == null)
                {
                    DateTime lower, upper;
                    foreach (DateRangeKind kind in PredefinedKinds)
                    {
                        GetRangeBounds(kind, out lower, out upper);
                        if (lower == _lowerBound && upper == _upperBound)
                        {
                            _kind = kind;
                            break;
                        }
                    }

                    if (_kind == null) _kind = DateRangeKind.Custom;
                }

                return _kind.Value;
            }
        }

        private DateTime _upperBound;
        [DisplayName("上限"), Description("指定日期范围的起始日期")]
        public DateTime UpperBound
        {
            get { return _upperBound; }
            set
            {
                if (_lowerBound != DateTime.MinValue && value != DateTime.MinValue && value < _lowerBound)
                    throw new ArgumentOutOfRangeException("结束日期不能早于开始日期", (Exception)null);

                _upperBound = value.Date;
                _kind = null;
            }
        }

        private DateTime _lowerBound;
        [DisplayName("下限"), Description("指定日期范围的结束日期")]
        public DateTime LowerBound
        {
            get { return _lowerBound; }
            set
            {
                if (_upperBound != DateTime.MinValue && value > _upperBound)
                    throw new ArgumentOutOfRangeException("开始日期不能晚于结束日期", (Exception)null);
                _lowerBound = value.Date;
                _kind = null;
            }
        }

        public bool IsEmpty
        {
            get { return !IsLowerSpecified && !IsUpperSpecified; }
        }

        public bool IsSingleDate
        {
            get { return _upperBound == _lowerBound; }
        }

        public bool IsLowerSpecified
        {
            get { return _lowerBound > DateTime.MinValue; }
        }

        public bool IsUpperSpecified
        {
            get { return _upperBound > DateTime.MinValue; }
        }

        public static DateRange Today
        {
            get
            {
                return new DateRange(DateTime.Today);
            }
        }

        public static DateRange ThisMonth
        {
            get
            {
                DateTime today = DateTime.Today;

                DateRange value = new DateRange();
                value.LowerBound = new DateTime(today.Year, today.Month, 1);
                value.UpperBound = value.LowerBound.AddMonths(1).AddDays(-1);

                return value;
            }
        }

        public static readonly DateRange Empty = new DateRange();

        internal const char SEPERATOR = ',';

        private static Dictionary<string, DateRangeKind> _kindLookup;

        public static DateRange Parse(string value)
        {
            if (value == null)
                throw new ArgumentNullException("value");

            int p = value.IndexOf(SEPERATOR);
            DateRange range;

            if (p >= 0)
            {
                string text;
                range = new DateRange();
                text = value.Substring(0, p).Trim();
                if (text.Length > 0)
                    range.LowerBound = DateTime.Parse(text);
                text = value.Substring(p + 1).Trim();
                if (text.Length > 0)
                    range.UpperBound = DateTime.Parse(text);
            }
            else
            {
                if (_kindLookup == null)
                {
                    _kindLookup = new Dictionary<string, DateRangeKind>(StringComparer.InvariantCultureIgnoreCase);
                    foreach (DateRangeKind kind in Enum.GetValues(typeof(DateRangeKind)))
                    {
                        _kindLookup[kind.ToString()] = kind;
                        _kindLookup[DateRangeConverter.GetKindText(kind)] = kind;
                    }
                }

                DateRangeKind k;
                if (_kindLookup.TryGetValue(value, out k))
                    return new DateRange(k);

                DateTime dt;

                if (!DateTime.TryParse(value, out dt))
                    throw new FormatException("Invalid date format");

                range = new DateRange(dt);
            }

            return range;
        }

        private static DayOfWeek _firstDayOfWeek = DayOfWeek.Monday;
            //System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek;

        public static DayOfWeek FirstDayOfWeek
        {
            get { return _firstDayOfWeek; }
            set { _firstDayOfWeek = value; }
        }

        public static void GetRangeBounds(DateRangeKind kind, out DateTime lower, out DateTime upper)
        {
            DateTime today = DateTime.Today, date = today;
            int number;

            switch (kind)
            {
                case DateRangeKind.Today:
                    lower = upper = today;
                    break;
                case DateRangeKind.Yestoday:
                    lower = upper = today.AddDays(-1);
                    break;
                case DateRangeKind.ThisMonth:
                    lower = new DateTime(date.Year, date.Month, 1);
                    upper = lower.AddMonths(1).AddDays(-1);
                    break;
                case DateRangeKind.LastMonth:
                    date = today.AddMonths(-1);
                    goto case DateRangeKind.ThisMonth;
                case DateRangeKind.LastWeek:
                    date = today.AddDays(-7);
                    goto case DateRangeKind.ThisWeek;
                case DateRangeKind.ThisWeek:
                    lower = date.AddDays(-(int)date.DayOfWeek + (int)DateRange.FirstDayOfWeek);
                    //Console.WriteLine(System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek);
                    upper = lower.AddDays(6);
                    break;
                case DateRangeKind.ThisQuarter:
                case DateRangeKind.Quarter1:
                case DateRangeKind.Quarter2:
                case DateRangeKind.Quarter3:
                case DateRangeKind.Quarter4:
                    if (kind == DateRangeKind.ThisQuarter)
                        number = today.Month / 3 * 3 + 1;
                    else
                        number = ((int)kind - (int)DateRangeKind.Quarter1) * 3 + 1;
                    lower = new DateTime(today.Year, number, 1);
                    upper = lower.AddMonths(3).AddDays(-1);
                    break;
                case DateRangeKind.ThisYear:
                    lower = new DateTime(date.Year, 1, 1);
                    upper = new DateTime(date.Year, 12, 31);
                    break;
                case DateRangeKind.LastYear:
                    date = today.AddYears(-1);
                    goto case DateRangeKind.ThisYear;
                default:
                    throw new ArgumentOutOfRangeException();
            }
            
        }

        // override object.Equals
        public override bool Equals(object obj)
        {
            //       
            // See the full list of guidelines at
            //   http://go.microsoft.com/fwlink/?LinkID=85237  
            // and also the guidance for operator== at
            //   http://go.microsoft.com/fwlink/?LinkId=85238
            //

            if (obj == null || GetType() != obj.GetType())
            {
                return false;
            }

            DateRange other = (DateRange)obj;
            return other._upperBound == _upperBound &&
                other._lowerBound == _lowerBound;
        }

        // override object.GetHashCode
        public override int GetHashCode()
        {
            return _lowerBound.GetHashCode() ^ _upperBound.GetHashCode();
        }

        public static bool operator == (DateRange v1, DateRange v2)
        {
            return v1.Equals(v2);
        }

        public override string ToString()
        {
            return ToString(false);
        }

        public string ToString(bool exactDate)
        {
            var range = this;

            if (range.IsEmpty) return string.Empty;
            if (!exactDate && range.Kind != DateRangeKind.Custom)
                return DateRangeConverter.GetKindText(range.Kind);

            StringBuilder text = new StringBuilder();
            if (!range.IsSingleDate)
            {
                if (range.IsLowerSpecified)
                    text.Append(range.LowerBound.ToShortDateString());
                text.Append(DateRange.SEPERATOR);
                if (range.IsUpperSpecified)
                    text.Append(range.UpperBound.ToShortDateString());
            }
            else
                text.Append(range.LowerBound.ToShortDateString());
            return text.ToString();
        }

        public static bool operator !=(DateRange v1, DateRange v2)
        {
            return !v1.Equals(v2);
        }


        object IRange.LowerBound
        {
            get
            {
                if (IsLowerSpecified) return LowerBound;
                else return null;
            }
        }

        object IRange.UpperBound
        {
            get
            {
                if (IsUpperSpecified)
                {
                    return UpperBound.AddDays(1).AddMinutes(-1);
                }
                else return null;
            }
        }
    }

    public enum DateRangeKind
    {
        [Description("自定义")]
        Custom,
        [Description("今天")]
        Today,
        [Description("昨天")]
        Yestoday,
        [Description("本月")]
        ThisMonth,
        [Description("上月")]
        LastMonth,
        [Description("本周")]
        ThisWeek,
        [Description("上周")]
        LastWeek,
        [Description("本季度")]
        ThisQuarter,
        [Description("一季度")]
        Quarter1,
        [Description("二季度")]
        Quarter2,
        [Description("三季度")]
        Quarter3,
        [Description("四季度")]
        Quarter4,
        [Description("今年")]
        ThisYear,
        [Description("去年")]
        LastYear,
    }

    public class DateRangeConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            if (sourceType == typeof(string))
                return true;
            return base.CanConvertFrom(context, sourceType);
        }

        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            if (destinationType == typeof(string))
                return true;
            return base.CanConvertTo(context, destinationType);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
        {
            if (value == null) return null;

            if (value is string)
            {
                string text = ((string)value).Trim();
                if (text.Length > 0)
                {
                    if (text.ToLower() == "empty")
                        return DateRange.Empty;

                    if (Enum.IsDefined(typeof(DateRangeKind), text))
                    {
                        DateRangeKind kind = (DateRangeKind)Enum.Parse(typeof(DateRangeKind), text, true);
                        return new DateRange(kind);
                    }

                    return DateRange.Parse(text);
                }
                else
                    return DateRange.Empty;
            }
            else if (value is DateTime)
            {
                return new DateRange((DateTime)value);
            }

            return base.ConvertFrom(context, culture, value);
        }

        public static string GetKindText(DateRangeKind kind)
        {
            var array = typeof(DateRangeKind).GetField(kind.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false);
            if (array.Length > 0) return ((DescriptionAttribute)array[0]).Description;
            else return kind.ToString();
        }

        public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
        {
            if (destinationType == typeof(string))
            {
                if (value == null) return null;
                DateRange range;
                if (value is DateRangeKind)
                    range = new DateRange((DateRangeKind)value);
                else
                    range = (DateRange)value;
                return range.ToString();
                //if (range.IsEmpty) return string.Empty;
                //if (range.Kind != DateRangeKind.Custom)
                //    return GetKindText(range.Kind);

                //StringBuilder text = new StringBuilder();
                //if (!range.IsSingleDate)
                //{
                //    if (range.IsLowerSpecified)
                //        text.Append(range.LowerBound.ToShortDateString());
                //    text.Append(DateRange.SEPERATOR);
                //    if (range.IsUpperSpecified)
                //        text.Append(range.UpperBound.ToShortDateString());
                //}
                //else
                //    text.Append(range.LowerBound.ToShortDateString());
                //return text.ToString();
            }
            return base.ConvertTo(context, culture, value, destinationType);
        }

        public override bool GetPropertiesSupported(ITypeDescriptorContext context)
        {
            return true;
        }

        public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
        {
            var coll = TypeDescriptor.GetProperties(typeof(DateRange));
            return new PropertyDescriptorCollection(new PropertyDescriptor[] {
                coll["LowerBound"],
                coll["UpperBound"]});
        }

        public override bool GetCreateInstanceSupported(ITypeDescriptorContext context)
        {
            return true;
        }

        public override object CreateInstance(ITypeDescriptorContext context, System.Collections.IDictionary propertyValues)
        {
            DateRange range = new DateRange();
            range.LowerBound = (DateTime)propertyValues["LowerBound"];
            range.UpperBound = (DateTime)propertyValues["UpperBound"];

            return range;
        }

        public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
        {
            return true;
        }

        public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
        {
            Array array = Enum.GetValues(typeof(DateRangeKind));
            
            return new StandardValuesCollection(array);
        }
    }
}
